#region Copyright
/// <copyright>
/// Copyright (c) 2007 Ilia Shramko.
/// Released under the terms of the GNU General Public License.
/// </copyright>
#endregion

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Net.Mime;
using System.Runtime.InteropServices;
using System.Text;

using Osh;

namespace OshHost.Web
{
	public class CacheEntry
	{
		#region WinInetCache Class
		public class WinInetCache
		{
			[Flags]
			public enum EntryType
			{
				// Fields
				Edited = 8,
				TrackOffline = 0x10,
				TrackOnline = 0x20,
				NormalEntry = 0x41,
				StickyEntry = 0x44,
				Sparse = 0x10000,
				Cookie = 0x100000,
				UrlHistory = 0x200000,
			}

			[Flags]
			public enum Entry_FC
			{
				// Fields
				None = 0,
				Attribute = 4,
				Hitrate = 0x10,
				Modtime = 0x40,
				Exptime = 0x80,
				Acctime = 0x100,
				Synctime = 0x200,
				Headerinfo = 0x400,
				ExemptDelta = 0x800,
			}

			[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
			public class EntryBuffer
			{
				public int StructSize;
				[MarshalAs(UnmanagedType.LPWStr)]
				public string SourceUrlName;
				[MarshalAs(UnmanagedType.LPWStr)]
				public string FileName;
				public EntryType EntryType;
				public int UseCount;
				public int HitRate;
				public int SizeLow;
				public int SizeHigh;
				public Win32.FILETIME LastModifiedTime;
				public Win32.FILETIME ExpireTime;
				public Win32.FILETIME LastAccessTime;
				public Win32.FILETIME LastSyncTime;
				public IntPtr HeaderInfoPtr;
				public int HeaderInfoChars;
				[MarshalAs(UnmanagedType.LPWStr)]
				public string Extension;
				public int ExemptDelta;

				public const int DefaultBufferSize = 0x800;

				public EntryBuffer()
				{
					this.StructSize = Marshal.SizeOf(typeof(EntryBuffer));
				}
			}

			[DllImport("wininet.dll", CharSet = CharSet.Unicode, SetLastError = true)]
			public static extern unsafe bool CommitUrlCacheEntry(
				[In] string urlName, [In] string localFileName,
				[In] Win32.FILETIME expireTime, [In] Win32.FILETIME lastModifiedTime,
				[In] EntryType EntryType, [In] string headerInfo, [In] int headerSizeTChars,
				[In] string fileExtension, [In] string originalUrl);

			[DllImport("wininet.dll", CharSet = CharSet.Unicode, SetLastError = true)]
			public static extern bool CreateUrlCacheEntry([In] string urlName, [In] int expectedFileSize,
				[In] string fileExtension, [Out] StringBuilder fileName, [In] int dwReserved);

			[DllImport("wininet.dll", CharSet = CharSet.Unicode, SetLastError = true)]
			public static extern bool DeleteUrlCacheEntry([In] string urlName);

			[DllImport("wininet.dll", CharSet = CharSet.Unicode, SetLastError = true)]
			public static extern unsafe bool GetUrlCacheEntryInfo(
				[In] string urlName, [In] IntPtr entryPtr, [In, Out] ref int bufferSz);

			[DllImport("wininet.dll", CharSet = CharSet.Unicode, SetLastError = true)]
			public static extern unsafe bool SetUrlCacheEntryInfo(
				[In] string lpszUrlName, [In] IntPtr entryPtr, [In] Entry_FC fieldControl);

			[DllImport("wininet.dll", CharSet = CharSet.Unicode, SetLastError = true)]
			public static extern bool UnlockUrlCacheEntryFile([In] string urlName, [In] int dwReserved);

			[DllImport("wininet.dll", SetLastError = true)]
			public static extern unsafe bool RetrieveUrlCacheEntryFile(
				[In] string urlName, [In] IntPtr entryPtr, [In, Out] ref int entryBufSize, [In] int dwReserved);

		}
		#endregion WinInetCache

		public string SourceUrlName;
		public string FileName;
		public WinInetCache.EntryType EntryType;
		public int UseCount;
		public int HitRate;
		public int SizeLow;
		public int SizeHigh;
		public DateTime LastModifiedTime;
		public DateTime ExpireTime;
		public DateTime LastAccessTime;
		public DateTime LastSyncTime;
		public string HeaderInfo;
		public string Extension;
		public int ExemptDelta;

		private CacheEntry()
		{
		}

		public byte[] ReadFile()
		{
			if(string.IsNullOrEmpty(this.FileName))
				throw new InvalidOperationException();

			int entrySize = 0;
			if(!WinInetCache.RetrieveUrlCacheEntryFile(this.SourceUrlName, IntPtr.Zero, ref entrySize, 0))
			{
				if(Marshal.GetLastWin32Error() != Win32.ERROR_INSUFFICIENT_BUFFER)
					Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());

				IntPtr entryBuffPtr = Marshal.AllocHGlobal(entrySize);
				try
				{
					if(!WinInetCache.RetrieveUrlCacheEntryFile(this.SourceUrlName, entryBuffPtr, ref entrySize, 0))
						Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
				}
				finally
				{
					Marshal.FreeHGlobal(entryBuffPtr);
				}
			}

			try
			{
				byte[] body = File.ReadAllBytes(this.FileName);
				return body;
			}
			finally
			{
				if(!WinInetCache.UnlockUrlCacheEntryFile(this.SourceUrlName, 0))
					Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
			}
		}

		public void Save()
		{
			IntPtr bufferPtr = IntPtr.Zero;
			WinInetCache.EntryBuffer buffer = new WinInetCache.EntryBuffer();
			try
			{
				buffer.SourceUrlName = this.SourceUrlName;
				buffer.FileName = this.FileName;
				buffer.EntryType = this.EntryType;
				buffer.UseCount = this.UseCount;
				buffer.HitRate = this.HitRate;
				buffer.SizeLow = this.SizeLow;
				buffer.SizeHigh = this.SizeHigh;
				buffer.LastModifiedTime = new Win32.FILETIME(ToFileTimeUtc(this.LastModifiedTime));
				buffer.ExpireTime = new Win32.FILETIME(ToFileTimeUtc(this.ExpireTime));
				buffer.LastAccessTime = new Win32.FILETIME(ToFileTimeUtc(this.LastAccessTime));
				buffer.LastSyncTime = new Win32.FILETIME(ToFileTimeUtc(this.LastSyncTime));

				if(this.HeaderInfo != null)
				{
					buffer.HeaderInfoPtr = Marshal.StringToHGlobalUni(this.HeaderInfo);
					buffer.HeaderInfoChars = this.HeaderInfo.Length;
				}
				else
				{
					buffer.HeaderInfoPtr = IntPtr.Zero;
					buffer.HeaderInfoChars = 0;
				}

				buffer.Extension = this.Extension;
				buffer.ExemptDelta = this.ExemptDelta;

				int bufferSize = buffer.StructSize;
				bufferPtr = Marshal.AllocHGlobal(bufferSize);
				Marshal.StructureToPtr(buffer, bufferPtr, false);

				WinInetCache.Entry_FC flags = 
					WinInetCache.Entry_FC.Acctime | WinInetCache.Entry_FC.Attribute |
					WinInetCache.Entry_FC.ExemptDelta | WinInetCache.Entry_FC.Exptime |
					WinInetCache.Entry_FC.Headerinfo | WinInetCache.Entry_FC.Hitrate |
					WinInetCache.Entry_FC.Modtime | WinInetCache.Entry_FC.Synctime;

				if(!WinInetCache.SetUrlCacheEntryInfo(this.SourceUrlName, bufferPtr, flags))
					Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
			}
			finally
			{
				if(buffer.HeaderInfoPtr != IntPtr.Zero)
					Marshal.FreeHGlobal(buffer.HeaderInfoPtr);
				if(bufferPtr != IntPtr.Zero)
					Marshal.DestroyStructure(bufferPtr, typeof(WinInetCache.EntryBuffer));
			}
		}

		public static CacheEntry Create(string url, string originalUrl, string fileExtension,
			DateTime expiresTime, DateTime lastModifiedTime, string headerInfo, byte[] body)
		{
			if(string.IsNullOrEmpty(url))
				throw new ArgumentNullException("url");

			try
			{
				StringBuilder fileNameSB = new StringBuilder(Win32.MAX_PATH);
				if(!WinInetCache.CreateUrlCacheEntry(url, body.Length, fileExtension, fileNameSB, 0))
					Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());

				string fileName = fileNameSB.ToString();

				File.WriteAllBytes(fileName, body);

				bool result = WinInetCache.CommitUrlCacheEntry(url, fileName,
					new Win32.FILETIME(ToFileTimeUtc(expiresTime)),
					new Win32.FILETIME(ToFileTimeUtc(lastModifiedTime)),
					WinInetCache.EntryType.NormalEntry,
					headerInfo, headerInfo == null ? 0 : headerInfo.Length,
					null, originalUrl);

				if(!result)
					Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());

				CacheEntry cacheEntry = GetEntry(url, true);
				return cacheEntry;
			}
			catch
			{
				WinInetCache.DeleteUrlCacheEntry(url);
				throw;
			}
		}

		public static void Delete(string url)
		{
			if(!TryDelete(url))
				Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
		}

		public static bool TryDelete(string url)
		{
			return WinInetCache.DeleteUrlCacheEntry(url);
		}

		public static bool TryGetEntry(string url, out CacheEntry entry)
		{
			entry = GetEntry(url, false);
			return entry != null;
		}

		private static CacheEntry GetEntry(string url, bool throwException)
		{
			int bufferSize = WinInetCache.EntryBuffer.DefaultBufferSize;
			for(int i = 0; i < 3; i++)
			{
				IntPtr memoryPtr = Marshal.AllocHGlobal(bufferSize);
				try
				{
					if(WinInetCache.GetUrlCacheEntryInfo(url, memoryPtr, ref bufferSize))
					{
						return CacheEntry.Load(memoryPtr);
					}

					if(Marshal.GetLastWin32Error() != Win32.ERROR_INSUFFICIENT_BUFFER)
					{
						if(throwException)
							Marshal.ThrowExceptionForHR(Marshal.GetHRForLastWin32Error());
						else
							return null;
					}
				}
				finally
				{
					Marshal.FreeHGlobal(memoryPtr);
				}
			}
			
			if(throwException)
				Marshal.ThrowExceptionForHR(Win32.ERROR_INSUFFICIENT_BUFFER);

			return null;
		}

		private static CacheEntry Load(IntPtr memoryPtr)
		{
			WinInetCache.EntryBuffer entryBuffer = new WinInetCache.EntryBuffer();
			Marshal.PtrToStructure(memoryPtr, entryBuffer);

			CacheEntry entry = new CacheEntry();
			entry.SourceUrlName = entryBuffer.SourceUrlName;
			entry.FileName = entryBuffer.FileName;
			entry.EntryType = entryBuffer.EntryType;
			entry.UseCount = entryBuffer.UseCount;
			entry.HitRate = entryBuffer.HitRate;
			entry.SizeLow = entryBuffer.SizeLow;
			entry.SizeHigh = entryBuffer.SizeHigh;
			entry.LastModifiedTime = DateTime.FromFileTimeUtc(entryBuffer.LastModifiedTime.ToLong());
			entry.ExpireTime = DateTime.FromFileTimeUtc(entryBuffer.ExpireTime.ToLong());
			entry.LastAccessTime = DateTime.FromFileTimeUtc(entryBuffer.LastAccessTime.ToLong());
			entry.LastSyncTime = DateTime.FromFileTimeUtc(entryBuffer.LastSyncTime.ToLong());

			if(entryBuffer.HeaderInfoPtr != IntPtr.Zero && entryBuffer.HeaderInfoChars > 0)
				entry.HeaderInfo = Marshal.PtrToStringUni(
					entryBuffer.HeaderInfoPtr, entryBuffer.HeaderInfoChars);

			entry.Extension = entryBuffer.Extension;
			entry.ExemptDelta = entryBuffer.ExemptDelta;

			return entry;
		}
	
		private static long ToFileTimeUtc(DateTime dateTime)
		{
			try
			{
				return dateTime.ToFileTimeUtc();
			}
			catch(ArgumentOutOfRangeException) {}
			return 0;
		}

	}
}
